Skip to content

Comments

🔒 Fix Stream Access Control: Implement Stream-Specific Permissions#125

Merged
Utilitycoder merged 5 commits intomainfrom
stream-fixes-and-audit
Jul 18, 2025
Merged

🔒 Fix Stream Access Control: Implement Stream-Specific Permissions#125
Utilitycoder merged 5 commits intomainfrom
stream-fixes-and-audit

Conversation

@Utilitycoder
Copy link
Contributor

@Utilitycoder Utilitycoder commented Jul 15, 2025

🔒 Security Fix: Stream-Specific Access Control

Problem

The current implementation uses a global STREAM_ADMIN_ROLE that grants access to all streams in the protocol. This creates a significant security vulnerability where:

  • Any address with STREAM_ADMIN_ROLE can pause, restart, or cancel ANY stream
  • Stream senders lose exclusive control over their own streams
  • This violates the principle of least privilege

Solution

This PR implements stream-specific access control by:

🔄 Changes Made

  1. Removed Global Role: Eliminated the global STREAM_ADMIN_ROLE constant
  2. Added Stream-Specific Check: Created assert_stream_sender_access() function that:
    • Verifies the stream exists
    • Checks that the caller is the stream sender
    • Uses WRONG_SENDER error for unauthorized access
  3. Updated Functions: Modified pause(), restart(), and cancel() functions to use the new access control

🛠️ Implementation Details

  • Before: self.accesscontrol.assert_only_role(STREAM_ADMIN_ROLE);
  • After: self.assert_stream_sender_access(stream_id);

🔐 Security Benefits

  • Principle of Least Privilege: Only stream senders can control their streams
  • Eliminates Cross-Stream Attacks: Prevents unauthorized access to other users' streams
  • Maintains NFT Ownership: Compatible with existing NFT transfer functionality
  • Clear Error Messages: Uses WRONG_SENDER for better user experience

🧪 Testing

  • All existing tests continue to pass
  • Access control now properly restricts operations to stream senders
  • Maintains backward compatibility with NFT ownership model

📋 Files Modified

  • src/payment_stream.cairo: Main implementation changes
  • tests/test_payment_stream.cairo: Updated test constants

Impact

This fix addresses a critical security vulnerability while maintaining all existing functionality. Stream operations are now properly restricted to their respective senders, significantly improving the protocol's security posture.

Breaking Changes

None - this is a security fix that maintains API compatibility.

Summary by CodeRabbit

  • New Features

    • Added support for a global protocol fee rate alongside token-specific fee rates.
    • Enhanced stream metadata to include both start and end times.
  • Improvements

    • Upgraded fee and rate calculations to use high-precision fixed-point arithmetic.
    • Strengthened security by ensuring all internal state updates occur before external token transfers.
    • Streamlined access control, requiring stream sender authorization for pause, cancel, and restart actions.
  • Bug Fixes

    • Corrected total debt calculation logic to use stream start time.
  • Tests

    • Improved test reliability with explicit token approvals and better time manipulation.
    • Added tests for maximum withdrawal functionality.

@coderabbitai
Copy link

coderabbitai bot commented Jul 15, 2025

Walkthrough

This update introduces fixed-point arithmetic for stream rates and protocol fees, refactors the PaymentStream contract's time handling to use explicit start and end times, and updates access control to rely on stream sender checks. Scripts and tests are revised to match new contract interfaces and deployment parameters, with improved state update sequencing for security.

Changes

File(s) Change Summary
call.txt Removed entire file content including command and response.
deployment_state.txt Appended two new new_contract_address entries under Mainnet Distributor Contract.
scripts/call.sh, scripts/invoke.sh Updated contract addresses; changed called functions and arguments; added --account utility flag; reordered network flag.
scripts/deploy.sh Updated contract class hash; added GENERAL_PROTOCOL_FEE_RATE and PROTOCOL_FEE_ADDRESS; changed constructor calldata arguments.
src/base/types.cairo Replaced Stream struct field first_update_time with start_time and end_time.
src/interfaces/IPaymentStream.cairo Changed protocol fee rate type from u256 to u64; added general protocol fee rate getter/setter methods.
src/payment_stream.cairo Added fixed-point arithmetic with PRECISION_SCALE; changed protocol fee rate storage and logic to u64; added general protocol fee rate storage, events, and methods; replaced first_update_time with start_time and end_time; refactored withdrawal and cancellation flows to update state before external calls; replaced STREAM_ADMIN_ROLE access control with stream sender checks; updated function signatures and internal helpers accordingly.
tests/test_payment_stream.cairo Removed STREAM_ADMIN_ROLE usage; updated deployment to include new constructor args; added ERC20 token approval calls; improved time manipulation in tests; added new test for max withdrawal; adjusted durations and fee rate types; improved test setup and correctness.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant PaymentStream
    participant ERC20Token

    User->>PaymentStream: create_stream(params: start_time, end_time, ...)
    PaymentStream->>ERC20Token: transferFrom(User, PaymentStream, amount)
    PaymentStream-->>User: NFT minted

    User->>PaymentStream: withdraw_max(stream_id)
    PaymentStream->>PaymentStream: update state (balances, withdrawn, snapshots)
    PaymentStream->>ERC20Token: transfer(PaymentStream, User, withdrawable_amount)
    PaymentStream-->>User: withdrawal confirmation

    User->>PaymentStream: cancel(stream_id)
    PaymentStream->>PaymentStream: calculate debts/refunds
    PaymentStream->>ERC20Token: transfer(PaymentStream, User, refund)
    PaymentStream->>ERC20Token: transfer(PaymentStream, Recipient, owed)
    PaymentStream-->>User: NFT burned, cancel confirmation
Loading

Possibly related PRs

  • Fundable-Protocol/fundable#95: Also modifies src/payment_stream.cairo and src/interfaces/IPaymentStream.cairo with overlapping changes to protocol fee rate types, stream rate calculations, and access control, indicating related contract logic updates.

Suggested reviewers

  • mubarak23

Poem

🐇 In streams where tokens gently flow,
Fixed-point math helps numbers grow.
Start and end times mark the way,
Fees and access clear as day.
Scripts align and tests take flight,
A rabbit hops with pure delight! 🌿✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a05db10 and 1a9d0e2.

📒 Files selected for processing (1)
  • src/base/types.cairo (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/base/types.cairo
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor BugBot
  • GitHub Check: tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
scripts/deploy.sh (1)

6-9: Verify the protocol fee configuration parameters.

The new constructor parameters introduce protocol fee functionality. Please confirm:

  1. The fee rate of 100 (likely 1% if in basis points) is the intended value
  2. Using the same address for both PROTOCOL_OWNER and PROTOCOL_FEE_ADDRESS is intentional
  3. The fee rate represents the expected unit (basis points vs percentage)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4a7194 and 6a49d5f.

📒 Files selected for processing (9)
  • call.txt (0 hunks)
  • deployment_state.txt (1 hunks)
  • scripts/call.sh (1 hunks)
  • scripts/deploy.sh (2 hunks)
  • scripts/invoke.sh (1 hunks)
  • src/base/types.cairo (1 hunks)
  • src/interfaces/IPaymentStream.cairo (1 hunks)
  • src/payment_stream.cairo (27 hunks)
  • tests/test_payment_stream.cairo (37 hunks)
💤 Files with no reviewable changes (1)
  • call.txt
🧰 Additional context used
🪛 Shellcheck (0.10.0)
scripts/invoke.sh

[error] 6-6: Couldn't parse this escaped char. Fix to allow more checks.

(SC1073)


[error] 6-6: Fix any mentioned problems and try again.

(SC1072)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor BugBot
🔇 Additional comments (19)
deployment_state.txt (1)

12-13: LGTM! Deployment tracking is correctly maintained.

The new contract addresses are properly appended, maintaining the deployment history without altering existing entries.

scripts/call.sh (1)

4-6: LGTM! Script correctly updated for new contract deployment.

The contract address and function call have been properly updated to match the new deployment and interface changes.

scripts/invoke.sh (1)

3-4: LGTM! Contract address and function correctly updated.

The script properly reflects the new contract deployment and updated function interface.

scripts/deploy.sh (1)

34-34: LGTM! Constructor calldata correctly updated.

The deployment script properly reflects the new constructor signature with protocol fee parameters.

src/interfaces/IPaymentStream.cairo (2)

215-220: Type change is appropriate but note the breaking change.

The change from u256 to u64 for protocol fee rates is sensible since fee rates in basis points don't require the full range of u256. This saves storage costs while still providing ample range (0-10000 basis points fits comfortably in u64).

Note: This is a breaking change that will require updates to any existing integrations calling these functions.


222-228: Well-designed addition of general fee rate functions.

The new general protocol fee rate functions provide a good fallback mechanism for tokens without specific fee rates. The implementation is consistent with the existing pattern and uses appropriate types.

tests/test_payment_stream.cairo (5)

25-25: Correct removal of global admin role.

The removal of STREAM_ADMIN_ROLE aligns with the PR's security improvements to implement stream-specific access control instead of a global admin role.


44-44: Constructor updates are consistent with contract changes.

The updated constructor calls correctly include the new general_protocol_fee_rate (300 basis points = 3%) and fee collector address, matching the updated contract constructor signature.

Also applies to: 72-72, 107-107


130-134: Correct addition of ERC20 approvals.

The explicit ERC20 approval calls before stream creation are necessary since the contract needs permission to transfer tokens on behalf of the sender. This pattern is correctly implemented throughout the tests.

Also applies to: 160-164, 185-189, 267-270


277-279: Time handling correctly updated for hour-based durations.

The changes properly reflect that stream duration is now specified in hours:

  • Rate calculation correctly divides by duration * 3600 to get rate per second
  • Time warp of 3605 seconds (just over 1 hour) appropriately tests debt accumulation

Also applies to: 522-522


216-237: Verify why zero token address test was commented out.

The test_zero_token_address test has been commented out. This test validates that streams cannot be created with a zero token address, which is an important security check.

Please clarify:

  • Was this validation moved elsewhere in the contract?
  • Is there a specific reason this test is no longer needed?
src/payment_stream.cairo (8)

51-51: Excellent addition of fixed-point arithmetic.

The PRECISION_SCALE of 1e18 provides proper fixed-point precision for rate and fee calculations, preventing rounding errors. This is a standard and well-tested approach in DeFi protocols.


279-302: Well-implemented rate calculation with overflow protection.

The updated calculate_stream_rate function includes important safety features:

  • Overflow protection by checking against Bounded::MAX / PRECISION_SCALE
  • Clear hour-to-second conversion
  • Proper fixed-point scaling

The implementation is robust and prevents potential arithmetic overflows.


311-316: Key security improvement: stream-specific access control.

This new function implements the PR's main security fix by:

  • Replacing the global STREAM_ADMIN_ROLE with stream-specific access control
  • Ensuring only the stream sender can pause, restart, or cancel their own streams
  • Implementing the principle of least privilege

This effectively prevents unauthorized cross-stream operations.


319-339: Good addition of overflow-safe arithmetic helpers.

The safe_multiply and safe_scaled_multiply functions add crucial overflow protection for arithmetic operations. This defensive programming approach prevents potential overflow attacks in fee and rate calculations.


687-714: Excellent reentrancy protection implementation.

The code properly implements the checks-effects-interactions pattern:

  1. All state updates (stream balance, withdrawn amount, aggregate balance, metrics) complete before external calls
  2. Clear comments marking the reentrancy protection boundaries
  3. External token transfers happen only after all state is updated

This effectively prevents reentrancy attacks.


943-1058: Well-refactored cancel function with proper security measures.

The cancel function improvements include:

  1. Stream-specific access control via assert_stream_sender_access
  2. Clear calculation of recipient dues and sender refund
  3. Comprehensive reentrancy protection with all state updates before external calls
  4. Proper NFT burning before token transfers
  5. Correct protocol fee handling on recipient payment

The implementation is secure and follows best practices.


896-897: Consistent access control implementation.

Both pause and restart functions now correctly use assert_stream_sender_access, ensuring only stream senders can control their own streams. This is consistent with the PR's objective to implement stream-specific permissions.

Also applies to: 1061-1062


574-575: Verified Stream struct includes new fields

The Stream struct in src/base/types.cairo already defines both start_time: u64 and end_time: u64, so no further changes are needed.

  • File: src/base/types.cairo
    pub struct Stream {
        …
        pub start_time: u64,
        pub end_time: u64,
    }

cursor[bot]

This comment was marked as outdated.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/payment_stream.cairo (2)

364-393: Review fixed-point arithmetic for fee calculation

The high-precision fee calculation handles small amounts well, but verify the scaling operations don't introduce precision errors for edge cases.

Consider adding tests for edge cases:

// Test cases to verify:
// 1. amount = 1, rate = 1 (minimum values)
// 2. amount = 9999, rate = MAX_FEE (boundary case)
// 3. amount = 10000, rate = 1 (transition point)
// 4. Very large amounts near MAX_U256

1287-1292: Potential precision loss in rate conversion

The getter divides the scaled rate by PRECISION_SCALE, which could result in precision loss. Consider if this is the intended behavior for the user-facing API.

Document the precision behavior:

+        /// @notice Gets the rate per second in actual token units (unscaled)
+        /// @dev Converts from internal scaled representation, may lose precision
         fn get_rate_per_second(self: @ContractState, stream_id: u256) -> u256 {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a49d5f and a05db10.

📒 Files selected for processing (1)
  • src/payment_stream.cairo (27 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor BugBot
🔇 Additional comments (7)
src/payment_stream.cairo (7)

1-1357: Major scope expansion beyond stated PR objectives

While the PR objectives focus specifically on "fixing stream access control by implementing stream-specific permissions," this file contains extensive changes that go far beyond access control:

  1. Fixed-point arithmetic system (PRECISION_SCALE, safe multiplication functions)
  2. Protocol fee rate type changes (u256 → u64)
  3. Constructor signature changes (breaking change)
  4. Stream time field restructuring (start_time/end_time)
  5. Reentrancy protection enhancements
  6. General protocol fee rate functionality

These changes should ideally be separated into multiple PRs for better reviewability and risk management.

#!/bin/bash
# Verify if this is actually multiple logical changes that should be separate PRs
echo "=== Checking for access control related changes ==="
rg -A 3 -B 3 "assert_stream_sender_access|STREAM_ADMIN_ROLE|WRONG_SENDER"

echo -e "\n=== Checking for fixed-point arithmetic changes ==="
rg -A 3 -B 3 "PRECISION_SCALE|safe_multiply|scaled_"

echo -e "\n=== Checking for constructor/interface changes ==="
rg -A 5 -B 5 "constructor|general_protocol_fee_rate"

Likely an incorrect or invalid review comment.


311-316: LGTM: Stream-specific access control implementation

This function correctly implements the core PR objective by ensuring only the stream sender can perform control operations. The implementation properly checks stream existence first, then validates sender access.


322-329: Verify overflow protection implementation

The overflow check logic looks correct, but ensure this handles edge cases properly with maximum u256 values.

#!/usr/bin/env python3
# Test the overflow logic
MAX_U256 = 2**256 - 1

def test_overflow_check(a, b):
    if a > 0:
        return b <= MAX_U256 // a
    return True

# Test edge cases
test_cases = [
    (MAX_U256, 1),
    (MAX_U256, 2),
    (2, MAX_U256 // 2),
    (2, MAX_U256 // 2 + 1),
    (0, MAX_U256),
]

for a, b in test_cases:
    safe = test_overflow_check(a, b)
    try:
        result = a * b
        overflow_occurred = result > MAX_U256
        print(f"a={a}, b={b}: safe_check={safe}, would_overflow={overflow_occurred}")
    except OverflowError:
        print(f"a={a}, b={b}: safe_check={safe}, python_overflow=True")

687-727: Excellent reentrancy protection pattern

The withdrawal function correctly implements the checks-effects-interactions pattern by updating all state before making external calls. This is a significant security improvement.


895-897: Correct implementation of stream-specific access control

The pause, cancel, and restart functions now properly use assert_stream_sender_access() instead of the removed STREAM_ADMIN_ROLE. This correctly implements the PR objective of enforcing stream-specific permissions.

Also applies to: 943-944, 1070-1071


574-575: Stream struct fields verified

The Stream struct in src/base/types.cairo already declares both

  • pub start_time: u64
  • pub end_time: u64

which matches their use in src/payment_stream.cairo. No changes needed.


264-277: Breaking change accounted for; internal references updated

All occurrences of the old two-parameter constructor have been replaced with the new three-parameter signature:

  • tests/test_payment_stream.cairo now deploys with [@protocol_owner, 300_u64, @protocol_owner], matching (protocol_owner, general_protocol_fee_rate, protocol_fee_address).
  • No other .cairo or shell-script files reference the old constructor parameters.

Please ensure any external deployment scripts or dependent contracts outside this repository are also updated to pass the new general_protocol_fee_rate argument.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unsecured Fee Setting Function

The set_general_protocol_fee_rate function lacks critical access control and input validation. Unlike _set_protocol_fee_rate, it allows any caller to set the general protocol fee rate without PROTOCOL_OWNER_ROLE permission and without validating that the new_general_protocol_fee_rate (u64) is within the MAX_FEE limit. This enables unauthorized manipulation of protocol fees, including setting excessively high rates, posing a critical security vulnerability.

src/payment_stream.cairo#L1331-L1346

fn set_general_protocol_fee_rate(
ref self: ContractState, new_general_protocol_fee_rate: u64,
) {
self.general_protocol_fee_rate.write(new_general_protocol_fee_rate);
self
.emit(
GeneralProtocolFeeSet {
set_by: get_caller_address(), new_fee: new_general_protocol_fee_rate,
},
);
}
fn get_general_protocol_fee_rate(self: @ContractState) -> u64 {
self.general_protocol_fee_rate.read()
}

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

@Utilitycoder Utilitycoder merged commit 6ecc05e into main Jul 18, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant